/********************************************************************++
Copyright (c) Microsoft Corporation.  All rights reserved.
--********************************************************************/

using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;

namespace System.Management.Automation
{
    internal class MUIFileSearcher
    {
        /// <summary>
        /// Constructor. It is private so that MUIFileSearcher is used only internal for this class. 
        /// To access functionality in this class, static api should be used. 
        /// </summary>
        /// <param name="target"></param>
        /// <param name="searchPaths"></param>
        /// <param name="searchMode"></param>
        private MUIFileSearcher(string target, Collection<String> searchPaths, SearchMode searchMode)
        {
            Target = target;
            SearchPaths = searchPaths;
            SearchMode = searchMode;
        }

        /// <summary>
        /// A constructor to make searchMode optional.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="searchPaths"></param>
        private MUIFileSearcher(string target, Collection<String> searchPaths)
            : this(target, searchPaths, SearchMode.Unique)
        {
        }

        #region Basic Properties

        /// <summary>
        /// Search target. It can be 
        ///     1. a file name
        ///     2. a search pattern
        /// It can also include a path, in that case, 
        ///     1. the path will be searched first for the existence of the files.
        /// </summary>
        internal string Target { get; } = null;

        /// <summary>
        /// Search path as provided by user.
        /// </summary>
        internal Collection<String> SearchPaths { get; } = null;

        /// <summary>
        /// Search mode for this file search. 
        /// </summary>
        internal SearchMode SearchMode { get; } = SearchMode.Unique;

        private Collection<String> _result = null;

        /// <summary>
        /// Result of the search.
        /// </summary>
        internal Collection<String> Result
        {
            get
            {
                if (_result == null)
                {
                    _result = new Collection<String>();

                    // SearchForFiles will fill the result collection.
                    SearchForFiles();
                }

                return _result;
            }
        }

        #endregion

        #region File Search

        /// <summary>
        /// _uniqueMatches is used to track matches already found during the search process.
        /// This is useful for ignoring duplicates in the case of unique search.
        /// </summary>
        private Hashtable _uniqueMatches = new Hashtable(StringComparer.OrdinalIgnoreCase);

        /// <summary>
        /// search for files using the target, searchPaths member of this class. 
        /// </summary>
        private void SearchForFiles()
        {
            if (String.IsNullOrEmpty(this.Target))
                return;

            string pattern = Path.GetFileName(this.Target);
            if (String.IsNullOrEmpty(pattern))
                return;

            Collection<String> normalizedSearchPaths = NormalizeSearchPaths(this.Target, this.SearchPaths);

            foreach (string directory in normalizedSearchPaths)
            {
                SearchForFiles(pattern, directory);

                if (this.SearchMode == SearchMode.First && this.Result.Count > 0)
                {
                    return;
                }
            }
        }

        private string[] GetFiles(string path, string pattern)
        {
#if UNIX
            // On Linux, file names are case sensitive, so we need to add
            // extra logic to select the files that match the given pattern.
            ArrayList result = new ArrayList();
            string[] files = Directory.GetFiles(path);
            foreach (string filePath in files)
            {
                if (filePath.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    result.Add(filePath);
                }
            }
            return (String[])result.ToArray(typeof(string));
#else
            return Directory.GetFiles(path, pattern);
#endif
        }

        private void AddFiles(string muiDirectory, string directory, string pattern)
        {
            if (Directory.Exists(muiDirectory))
            {
                string[] files = GetFiles(muiDirectory, pattern);

                if (files == null)
                    return;

                foreach (string file in files)
                {
                    string path = Path.Combine(muiDirectory, file);

                    switch (this.SearchMode)
                    {
                        case SearchMode.All:
                            _result.Add(path);
                            break;

                        case SearchMode.Unique:
                            // Construct a Unique filename for this directory.
                            // Remember the file may belong to one of the sub-culture 
                            // directories. In this case we should not be returning
                            // same files that are residing in 2 or more sub-culture
                            // directories.
                            string leafFileName = Path.GetFileName(file);
                            string uniqueToDirectory = Path.Combine(directory, leafFileName);

                            if (!_uniqueMatches.Contains(uniqueToDirectory))
                            {
                                _result.Add(path);
                                _uniqueMatches[uniqueToDirectory] = true;
                            }
                            break;

                        case SearchMode.First:
                            _result.Add(path);
                            return;

                        default:
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Search for files of a particular pattern under a particular directory. 
        /// This will do MUI search in which appropriate language directories are 
        /// searched in order. 
        /// </summary>
        /// <param name="pattern"></param>
        /// <param name="directory"></param>
        private void SearchForFiles(string pattern, string directory)
        {
            List<string> cultureNameList = new List<string>();
            CultureInfo culture = CultureInfo.CurrentUICulture;

            while (culture != null && !String.IsNullOrEmpty(culture.Name))
            {
                cultureNameList.Add(culture.Name);
                culture = culture.Parent;
            }
            cultureNameList.Add("");

            // Add en-US and en as fallback languages
            if (!cultureNameList.Contains("en-US"))
            {
                cultureNameList.Add("en-US");
            }
            if (!cultureNameList.Contains("en"))
            {
                cultureNameList.Add("en");
            }

            foreach (string name in cultureNameList)
            {
                string muiDirectory = Path.Combine(directory, name);

                AddFiles(muiDirectory, directory, pattern);

                if (this.SearchMode == SearchMode.First && this.Result.Count > 0)
                {
                    return;
                }
            }
            return;
        }

        /// <summary>
        /// A help file is located in 3 steps
        ///     1. If file itself contains a path itself, try to locate the file
        ///        from path. LocateFile will fail if this file doesn't exist.
        ///     2. Try to locate the file from searchPaths. Normally the searchPaths will
        ///        contain the cmdlet/provider assembly directory if currently we are searching
        ///        help for cmdlet and providers.
        ///     3. Try to locate the file in the default PowerShell installation directory.
        /// </summary>
        /// <param name="target"></param>
        /// <param name="searchPaths"></param>
        /// <returns></returns>
        private static Collection<String> NormalizeSearchPaths(string target, Collection<String> searchPaths)
        {
            Collection<String> result = new Collection<String>();

            // step 1: if target has path attached, directly locate 
            //         file from there.
            if (!String.IsNullOrEmpty(target) && !String.IsNullOrEmpty(Path.GetDirectoryName(target)))
            {
                string directory = Path.GetDirectoryName(target);

                if (Directory.Exists(directory))
                {
                    result.Add(Path.GetFullPath(directory));
                }

                //user specifically wanted to search in a particular directory
                //so return..
                return result;
            }

            // step 2: add directories specified in to search path.
            if (searchPaths != null)
            {
                foreach (string directory in searchPaths)
                {
                    if (!result.Contains(directory) && Directory.Exists(directory))
                    {
                        result.Add(directory);
                    }
                }
            }

            // step 3: locate the file in the default PowerShell installation directory.
            string defaultPSPath = GetMshDefaultInstallationPath();
            if (defaultPSPath != null &&
                !result.Contains(defaultPSPath) &&
                Directory.Exists(defaultPSPath))
            {
                result.Add(defaultPSPath);
            }

            return result;
        }

        /// <summary>
        /// Helper method which returns the default monad installation path based on ShellID
        /// registry key.
        /// </summary>
        /// <returns>string representing path.</returns>
        /// <remarks>
        /// If ShellID is not defined or Path property is not defined returns null.
        /// </remarks>
        private static string GetMshDefaultInstallationPath()
        {
            string returnValue = CommandDiscovery.GetShellPathFromRegistry(Utils.DefaultPowerShellShellID);

            if (returnValue != null)
            {
                returnValue = Path.GetDirectoryName(returnValue);
            }

            // returnValue can be null.
            return returnValue;
        }

        #endregion

        #region Static API's

        /// <summary>
        /// Search for files in default search paths.
        /// </summary>
        /// <param name="pattern"></param>
        /// <returns></returns>
        internal static Collection<String> SearchFiles(string pattern)
        {
            return SearchFiles(pattern, new Collection<String>());
        }

        /// <summary>
        /// Search for files in specified search paths.
        /// </summary>
        /// <param name="pattern"></param>
        /// <param name="searchPaths"></param>
        /// <returns></returns>
        internal static Collection<String> SearchFiles(string pattern, Collection<String> searchPaths)
        {
            MUIFileSearcher searcher = new MUIFileSearcher(pattern, searchPaths);

            return searcher.Result;
        }

        /// <summary>
        /// Locate a file in default search paths
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        internal static string LocateFile(string file)
        {
            return LocateFile(file, new Collection<String>());
        }

        /// <summary>
        /// Get the file in different search paths corresponding to current culture. 
        /// 
        /// The file name to search is the filename part of path parameter. (Normally path
        /// parameter should contain only the filename part. But it is possible for 
        /// RunspaceConfiguration to directly specify a hard coded path for help file there).
        /// 
        /// </summary>
        /// <param name="file">This is the path to the file. If it has a path, we need to search under that path first</param>
        /// <param name="searchPaths">Additional search paths</param>
        /// <returns></returns>
        internal static string LocateFile(string file, Collection<String> searchPaths)
        {
            MUIFileSearcher searcher = new MUIFileSearcher(file, searchPaths, SearchMode.First);

            if (searcher.Result == null || searcher.Result.Count == 0)
                return null;

            return searcher.Result[0];
        }

        #endregion
    }

    /// <summary>
    /// This enum defines different search mode for the MUIFileSearcher
    /// </summary>
    internal enum SearchMode
    {
        // return the first match
        First,

        // return all matches, with duplicates allowed
        All,

        // return all matches, with duplicates ignored
        Unique
    }
}

